【20年品牌建站】找北京网站建设公司就选新鸿儒/提供北京网站建设报价/北京网站制作/北京网站设计/网站开发、北京网站建设公司电话【010-51267718】有优惠哦!
简体
繁体 简体
我们的服务遍布中国

我们的服务遍布中国
乃至世界

新鸿儒所服务的品牌地域与城市
北京 天津 上海 广州 深圳 香港 厦门 江苏 浙江 山东
重庆 长沙 武汉 成都 西安 宁夏 丽江 青海 云南 乌鲁木齐
黑龙江 内蒙古 河北 ...
新鸿儒服务与合作的全球各地
美国 加拿大 德国 法国 英国 瑞士 意大利 荷兰
印度 日本 韩国 ...

不论你的品牌在何处
我们都可以提供完善的服务与帮助

致电

010-51267718

Godaddy服务器上关于ASP.NET网站建设一些经验 - 断点续传下载 (二)

发布时间:2013-11-22 浏览:124打印字号:

 谈谈在APS.NET中如何控制文件下载.

设计目的和要求

假设这么一个应用场景:
一个主机,上面存有许多文件资料,有各种文件格式.(PDF, DOC, EXE ... 等等).
该主机上运行一个ASP.NET网站, 用户注册,并付费之后允许他/她下载资料.

文件是放在IIS服务器上的, 如果用户知道具体路径那么他是可以随时下载的. (在没有或者不能设置访问权限的情况下.)
如果直接把下载路径发送给付费用户,肯定是行不通的,会被散播出去. 所以不能把让客户端得知具体路径,文件内容由 ASP.NET 服务器页面读取后发送给客户端. 
 
要做的就是: 编写一个ASP.NET 页面服务器代码, 读取指定文件,并发送给客户 .

总体思路

.net 里, 有2个函数可以用来发送文件 Response.WriteFile 和 Response.TransmiteFile
它们的主要区别是: WriteFile 是先把文件内容读取到服务器缓冲,然后再发送到客户端. 所以对于大文件,会造成服务器很大的压力.
一般用来处理小文件,比如,发送给 excel 报表之类的. TransmiteFile 不缓冲数据, 直接抛给客户端, 所以可以用来发大文件.
( 采用 TransmiteFile 来实现.)

具体实现

1. 给客户一个链接,形如 http://xxxx/downloads.aspx?Key=ABCD123456

2. 在downloads.aspx的服务器代码中, 通过Key的值,查询数据库,得到服务器上的真实文件路径. 这个时候,控制权在 downloads.aspx, 所以可以编写复杂的控制功能, 比如看看用户有没有登录,有没有付费之类的,从而避免外部盗链.

3. 得到文件路径后,调用 Response.TransmiteFile 发送文件给客户端.

4. 因为给客户的链接里没有任何文件名的信息, 所以要在HTTP响应头里添加一句,告诉客户端文件名:  Response.AddHeader("Content-Disposition", "attachment; filename=/"" + 你的文件名 + "/""); (如果要支持中文,要考虑编码的问题, 我这里不说,不是我们的主题.)

5. 如果是一个大文件, 比如1G, 不支持断点续传,是没有意义的. 那么如何实现呢?

(1) 要让客户端知道我们的服务器支持断点续传, 要在HTTP响应头中包含 Accept-Ranges: bytes 和 ETag: "XXXX".
 ETag 是一个文件的标识, 供客户端判断它请求的是同一个文件, ETag 的内容在HTTP规范里并没有具体要求,只要保证在同一个服务器上,同一个文件有相同的ETag 就行了, 一般就根据文件名和最后修改时间生成一个字符串就可以了.
 
代码示例:
Response.AddHeader("Accept-Ranges", "bytes");  // 断点续传控制.
Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传


(2) 要处理客户端请求中的 "Range" 字段. 一般格式是这样: Range: bytes=1234- 或者 Range: bytes=1234-12345
分别表示从地1235个字节开始下载和下载第1235到第12346个字节之间的数据.
服务器首先要添加 Content-Range 响应头, 然后用 TransmiteFile 发送指定的数据.

代码示例:
Response.StatusCode = 206;
Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length));  // 参数0 和 参数1 是位置. 参数2是文件长度
Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);

( 其中, lFrom 和 lTo 是根据客户端请求中的 Range 字段得到的.)
 

说一下优缺点:

1. 可以随心所欲的控制下载.
2. 可以绕过服务器文件类型下载的限制, 比如服务器不允许下载 ISO 和 NRG 文件扩展名的文件, 如果直接输入RUL会提示404, 但是用上述的方法可以下载.

3.用这种办法的话,下载是在.net的一个线程里做的,如果用户量大的话,需要维护多个响应

附注:

1. TransmitFile(String) ( 函数是 .net 2.0 才加上去的.

2. TransmitFile(String, Int64, Int64) 带发送位置参数的重载是 .net 2.0 sp1 以后才支持的. 所以要用本文所说的方法实现断点续传, 至少要支持.net 2.0 sp1

3. 没有检测请求头中的 If-Range 和 Unless-Modified-Since, 如果有需要,在得到文件名之后就可以校验一下, 分别对应 ETag 和 Last-Modified.


              // 1. 获取服务器上的文件路径 // 这里,如果文件路径有问题, 无法映射则会抛出异常, strURL 是根据 Key从数据库中查询到的真实文件路径
                  string strFilePath = Server.MapPath("~" + strURL);
                 
                  // 2. 获取文件名
                  string strFileName = System.IO.Path.GetFileName(strFilePath);

                  // 3. 确认文件是否存在
                  FileInfo fi = new FileInfo(strFilePath);
                  if (!fi.Exists)
                  {
                      // 退出点,文件不存在
                  }

                  // 4. 抛给客户端
                  strFileName.Replace(" ", "%20"); // 处理文件名含空格的情况
                  string strETag = strFileName.ToUpper() + ":" + fi.Length.ToString();  // 我的Etag 是用文件名和字节数构成,马马虎虎凑合用.
                  string strLastTime = fi.LastWriteTimeUtc.ToString("r");

                  Response.Clear();  // 先把响应流清空
                  Response.ContentType = "application/octet-stream";  // 指定文件类型,使客户端总是弹出保存文件的框框.
                  Response.AddHeader("Content-Disposition", "attachment; filename=/"" + strFileName + "/"");
                  Response.AddHeader("Accept-Ranges", "bytes");  // 断点续传控制.
                  Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传
                  Response.AddHeader("Last-Modified", strLastTime);//把最后修改日期写入响应

                  // 获取客户端请求的范围, 并且要校验这个范围的有效性
                  long lFrom = 0;
                  long lTo = 0;
                  bool bParts = false;
                  string strRange = Request.Headers["Range"];
                  if (ParseRange(strRange, out lFrom, out lTo))  /// ParseRange 是我自己写的函数, 从 Range 中读取2个位置.代码在后面.
                  {
                      if (-1 == lFrom && -1 == lTo)
                      {
                          // 不允许2个值都不指定
                      }
                      else
                      {
                          if (lTo == -1) lTo = fi.Length - 1;  // 客户端未指定结束位置,则认为是文件的最后一个字符 Range: bytes=123- 的情况
                          if (lFrom == -1) // Range: bytes=-123 的情况, 请求最后的123个字节
                          {
                              lFrom = fi.Length - lTo;
                              lTo = fi.Length - 1;
                          }

                          if (lFrom < 0 || lFrom >= fi.Length || lFrom > lTo || lTo < 0 || lTo >= fi.Length)
                          {
                              // 以上几种情况下,范围的值能解析出来,但是不合法.
                              // 首先 From 和 To 的下标都应该在文件长度范围内
                              // 其次 From 应该 <= To
                          }
                          else
                          {
                              bParts = true;
                          }
                      }
                  }

                  // 根据用户请求,返回数据段或者整个文件
                  if(bParts)
                  {
                      Response.StatusCode = 206;
                      Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
                      Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length));  // 参数0 和 参数1 是位置,从0开始. 参数2是文件长度
                      Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);
                  }
                  else
                  {
                      Response.AddHeader("Content-Length", fi.Length.ToString());
                      Response.TransmitFile(strFilePath);
                  }
                  Response.End();
              }


=============================传说中的分割线======================================
protected bool ParseRange(string strRange, out long lFrom, out long lTo)
    {
        lFrom = 0;
        lTo = 0;
        long lTemp = 0;
        if (strRange == null || strRange == "")
        {
            return false; // 字符串为空
        }
        else
        {
            strRange = strRange.Replace(" ", ""); // 去除多余的空格
            string[] range = strRange.Split(new char[] { '=', '-' });

            // 1.分割后,包含3段 第一段是 "Range: bytes", 第二段是起始位置, 第三段是结束位置
            if (range.Length != 3)
            {
                return false; // 格式不正确 只支持 Range: bytes=89294317- 或者 Range: bytes=1234-1235 或者 Range: bytes=-500 3种格式.
            }

            // 2. 解析起始位置
            if (range[1].Length <= 0)
            {
                // 起始位置未指定
                lFrom = -1;
            }
            else
            {
                if (!long.TryParse(range[1], out lTemp))
                {
                    return false; // 起始位置无法解析
                }
                lFrom = lTemp;
            }

            // 3. 解析结束位置
            if (range[2].Length <= 0)
            {
                lTo = -1; // 没有指定结束位置 Range: bytes=1234- 的情况
            }
            else
            {
                if (!long.TryParse(range[2], out lTemp))  // 排除 byte=xxxx- 的情况 TryParse 失败, 会把lTemp 置零
                {
                    return false; // 第三度的内容不为空,但是无法解析
                }
                lTo = lTemp;
            }
            return true;
        }
    }

现在就与新鸿儒客服交流

010-51267718

您也可进行在线咨询或预约项目顾问
我要预约
在线咨询